//---------------------------------------------------------------------------------
// Copyright (c) 2008 VoiceLynx Project
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
//     1. The origin of this software must not be misrepresented; you must not
//        claim that you wrote the original software. If you use this software
//        in a product, an acknowledgment in the product documentation would be
//        appreciated but is not required.
//
//     2. Altered source versions must be plainly marked as such, and must not be
//        misrepresented as being the original software.
//
//     3. This notice may not be removed or altered from any source
//        distribution.
//---------------------------------------------------------------------------------

#include "VLSoundManager.h"
#include <sstream>
#include <algorithm>

#include "fmod_errors.h"

#define SOUND_THROW(str)  throw std::runtime_error(str)

#define FMOD_CHECK(result)                                                          \
  if (result != FMOD_OK)                                                            \
  {                                                                                 \
    std::stringstream ss;                                                           \
    ss << "FMOD error! (" << result << ") " << FMOD_ErrorString(result) << "\n";    \
    SOUND_THROW(ss.str());                                                          \
  }



void fillFMODStructure(FMOD_CREATESOUNDEXINFO &exinfo)
{
  memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
  exinfo.cbsize           = sizeof(FMOD_CREATESOUNDEXINFO);
  exinfo.numchannels      = 1;
  exinfo.format           = FMOD_SOUND_FORMAT_PCM16;
  exinfo.defaultfrequency = VL_FMOD_SMAPLE_RATE;
  exinfo.length			      = VL_FMOD_FRAME_SIZE_IN_BYTES * VL_FMOD_FRAMES_IN_SOUND;
}

FMOD::Sound *createFModVoiceStreamSound(FMOD::System *system)
{
  FMOD::Sound *sound = 0;
  FMOD_CREATESOUNDEXINFO exinfo;
  fillFMODStructure(exinfo);

  FMOD_CHECK(system->createSound(0, FMOD_2D | FMOD_SOFTWARE | FMOD_OPENUSER | FMOD_LOOP_NORMAL, &exinfo, &sound));
  
  return sound;
}

float calculateVolumeOfFrames(VLByteArrayPtrList &byteArrayList)
{
  // default return value
  float volume = 0.0f;

  // If we have data
  if (byteArrayList.empty() == false)
  {
    // For every chunk of sound data
    for (VLByteArrayPtrList::iterator iter = byteArrayList.begin();
      iter != byteArrayList.end(); ++iter)
    {
      long volumeAccumulator = 0;

      // I assume that size of sample is greater than VLByte and that it 
      // is evenly divisible
      VL_COMPILER_ASSERT(sizeof(SAMPLE) / sizeof(VLByte) > 0 && sizeof(SAMPLE) % sizeof(VLByte) == 0);

      VLU16 *data = (VLU16 *) &(**iter)[0];
      VLU32 length = (*iter)->size() / (sizeof(SAMPLE) / sizeof(VLByte));

      for (VLU32 i = 0; i < length; ++i)
      {
        volumeAccumulator += data[i];
      }

      volume += ((float)volumeAccumulator / (float)(*iter)->size());
    }

    // Divide by the number of frames we measured, then make it range from [0.0, 1.0]
    volume = (volume / (float)byteArrayList.size()) / VL_FMOD_SAMPLE_MAX_LEVEL;
  }

  return volume;
}

//////////////////////////// RECORDER ////////////////////////
VLFModSoundRecorder::VLFModSoundRecorder(VLFModSoundEnv &soundEnv)
: mSoundEnv(soundEnv),
  mSound(0),
  mChannel(0),
  mLastRecordPos(0),
  mActivityLevel(0.0f),
  mGain(1.0f)
{
  // Create a sound to record with
  mSound = createFModVoiceStreamSound(mSoundEnv.getSystem());

  // The incomming sound records indefinately
  FMOD_CHECK(mSoundEnv.getSystem()->recordStart(mSound, true));

  // The incomming sound plays indefinately but is muted
  FMOD_CHECK(mSoundEnv.getSystem()->playSound(FMOD_CHANNEL_REUSE, mSound, false, &mChannel));
  FMOD_CHECK(mChannel->setVolume(0.0f));
}

VLFModSoundRecorder::~VLFModSoundRecorder()
{
  FMOD::System *system = mSoundEnv.getSystem();

  if (system != 0)
  {
    // Stop recording
    bool recording = false;
    FMOD_CHECK(system->isRecording(&recording));

    if (recording)
    {
      FMOD_CHECK(system->recordStop());
    }

    // Stop playing the recorded sound
    bool playing;
    FMOD_CHECK(mChannel->isPlaying(&playing));

    if (playing)
    {
      mChannel->stop();
    }

    // Release the sound we're recording into
    if (mSound != 0)
    {
      mSound->release();
      mSound = 0;
    }
  }
}

void VLFModSoundRecorder::pullData(VLByteArrayPtrList &byteArray)
{
  const int sampleSize = 2;

  // We remember where we were recording, and we get where we are recording now
  unsigned int& lastPos = mLastRecordPos;
  unsigned int currPos;
  unsigned int soundLength;

  // We need to figure out where the recording's write pointer ends
  // and where the sound object's buffer ends
  FMOD_CHECK(mSoundEnv.getSystem()->getRecordPosition(&currPos));
  FMOD_CHECK(mSound->getLength(&soundLength, FMOD_TIMEUNIT_PCM));

  int bufferSizeInBytes = VL_FMOD_FRAME_SIZE_IN_BYTES;

  // Round down to a multiple of the buffer size in samplesd
  currPos -= currPos % (bufferSizeInBytes / sampleSize);

  if (currPos != lastPos)
  {
    void *ptr1, *ptr2;
    unsigned int len1, len2;
    int blockLength;

    // Figure out how much data awaits, and account for a circular buffer wrap
    blockLength = (int)currPos - (int)lastPos;
    if (blockLength < 0)
    {
      blockLength += soundLength;
    }

    // Lock to get access to the raw data (we have two blocks to deal with due to our
    // circular buffer possibly wrapping)
    mSound->lock(lastPos * sampleSize, blockLength * sampleSize, 
      &ptr1, &ptr2, &len1, &len2);

    // Since the length and current position are both a multiple of bufferSizeBytes
    // just treat treat one full buffer at a time
    int numFrames1 = len1 / bufferSizeInBytes;
    int numFrames2 = len2 / bufferSizeInBytes;

    // Get the first chunk frame by frame
    while(numFrames1--){
      VLByteArrayPtr frame(new VLByteArray(bufferSizeInBytes));
      memcpy(&(*frame.get())[0], ptr1, bufferSizeInBytes);

      ptr1 = (char*)ptr1 + bufferSizeInBytes;

      byteArray.push_back(frame.release());
    }

    // Get the second chunk frame by frame
    while(numFrames2--) {
      VLByteArrayPtr frame(new VLByteArray(bufferSizeInBytes));
      memcpy(&(*frame.get())[0], ptr1, bufferSizeInBytes);

      ptr2 = (char*)ptr2 + bufferSizeInBytes;

      byteArray.push_back(frame.release());
    }

    // Release the raw sound data
    mSound->unlock(ptr1, ptr2, len1, len2);

    // Analyze the data to update our audio level
    mActivityLevel = calculateVolumeOfFrames(byteArray);
  }

  // Remember where we stopped reading so we can get that position later
  lastPos = currPos;
}

float VLFModSoundRecorder::getActivityLevel()
{
  return mActivityLevel;
}

float VLFModSoundRecorder::getGain()
{
  return mGain;
}

void VLFModSoundRecorder::setGain(float gainAmount)
{
  mGain = gainAmount;
}

/////////////////////////// Player //////////////////////////////
VLFModSoundStreamPlayer::VLFModSoundStreamPlayer(VLFModSoundEnv &soundEnv)
: mSoundEnv(soundEnv),
  mIncomingSoundData(VL_FMOD_FRAME_SIZE_IN_BYTES * 4),
  mSound(0),
  mChannel(0),
  mLastPlayPos(0),
  mGain(1.0f)
{
  // Create a sound to fill
  mSound = createFModVoiceStreamSound(soundEnv.getSystem());

  // The outgoing sound plays indefinately
  FMOD_CHECK(soundEnv.getSystem()->playSound(FMOD_CHANNEL_REUSE, mSound, false, &mChannel));
}

VLFModSoundStreamPlayer::~VLFModSoundStreamPlayer()
{
  FMOD::System *system = mSoundEnv.getSystem();

  if (system != 0)
  {
    // Stop playing incoming sound
    bool playing;
    FMOD_CHECK(mChannel->isPlaying(&playing));

    if (playing)
    {
      mChannel->stop();
    }

    if (mSound != 0)
    {
      mSound->release();
      mSound = 0;
    }
  }

  mSoundEnv.unregisterSoundStreamPlayer(this);
}

void VLFModSoundStreamPlayer::pushData(VLCircularBuffer &mIncommingBuffer)
{
  // In this function, we need to pull data out of the incomming circular buffer
  // and put it in the playing sound

  const int sampleSize = 2;

  // We remember where we were recording, and we get where we are recording now
  unsigned int& lastPos = mLastPlayPos;
  unsigned int currPos;

  FMOD_CHECK(mChannel->getPosition(&currPos, FMOD_TIMEUNIT_PCM));

  unsigned int soundLength;

  // Get samples we need to copy
  FMOD_CHECK(mSound->getLength(&soundLength, FMOD_TIMEUNIT_PCM));

  VLU32 bufferSizeInBytes = VL_FMOD_FRAME_SIZE_IN_BYTES;

  // Round down to a multiple of the buffer size in samples
  currPos -= currPos % (bufferSizeInBytes / sampleSize);

  if (currPos != lastPos)
  {
    void *ptr1, *ptr2;
    unsigned int len1, len2;
    int blockLength;

    blockLength = (int)currPos - (int)lastPos;
    // Check for wrap around, and adjust
    if (blockLength < 0)
    {
      blockLength += soundLength;
    }

    // Lock to get access to the raw data (we have two blocks to deal with due to our
    // circular buffer possibly wrapping
    mSound->lock(lastPos * sampleSize, blockLength * sampleSize, 
      &ptr1, &ptr2, &len1, &len2);

    // Since the length and current position are both a multiple of bufferSizeBytes
    // just treat treat one full buffer at a time
    int numFrames = len1 / bufferSizeInBytes;
    while(numFrames--)
    {
      if (mIncommingBuffer.pending() >= bufferSizeInBytes)
      {
        bool success = mIncommingBuffer.consume((VLByte*) ptr1, bufferSizeInBytes);
      }
      else
      {
        memset(ptr1, 0, bufferSizeInBytes);
      }

      ptr1 = (char*)ptr1 + bufferSizeInBytes;
    }

    numFrames = len2 / bufferSizeInBytes;
    while(numFrames--) 
    {
      if (mIncommingBuffer.pending() >= bufferSizeInBytes)
      {
        bool success = mIncommingBuffer.consume((VLByte*) ptr2, bufferSizeInBytes);
      }
      else
      {
        memset(ptr1, 0, bufferSizeInBytes);
      }

      ptr2 = (char*)ptr2 + bufferSizeInBytes;
    }

    mSound->unlock(ptr1, ptr2, len1, len2);
  }

  lastPos = currPos;
}

float VLFModSoundStreamPlayer::getActivityLevel()
{
  return 0.0f;
}

float VLFModSoundStreamPlayer::getGain()
{
  return mGain;
}

void VLFModSoundStreamPlayer::setGain(float gainAmount)
{
  mGain = gainAmount;
  mChannel->setVolume(mGain);
}

void VLFModSoundStreamPlayer::process()
{
  pushData(mIncomingSoundData);
}

void VLFModSoundStreamPlayer::giveData( VLByteArrayPtr data )
{
  mIncomingSoundData.give(data);
}

///////////////////////// Static Sound //////////////////////////

VLFModStaticSound::VLFModStaticSound(VLFModSoundEnv &soundEnv, const VLString &source)
: mSoundEnv(soundEnv),
  mSound(0)
{
  FMOD_CHECK(mSoundEnv.getSystem()->createSound(source.c_str(), FMOD_DEFAULT, 0, &mSound));
}

VLFModStaticSound::~VLFModStaticSound()
{
  // It may be unnecessary to stop a channel before deleting a sound (depends if fmod
  // does reference counting - my guess is it does, but there's no harm in stopping
  // the sound)
  // stop();

  if (mSound != 0)
  {
    FMOD_CHECK(mSound->release());
  }
}

void VLFModStaticSound::play()
{
  //stop();

  FMOD_CHECK(mSoundEnv.getSystem()->playSound(FMOD_CHANNEL_FREE, mSound, false, 0 /*mChannel*/ ));
}

// void VLFModStaticSound::stop()
// {
//   if (mChannel != 0)
//   {
//     if (mChannel->isPlaying())
//     {
//       mChannel->stop();
//       mChannel = 0;
//     }
//   }
// }

//////////////////////////// Manager ////////////////////////////

VLFModSoundManager::VLFModSoundManager()
: mOutgoingBuffer(VL_FMOD_FRAME_SIZE_IN_BYTES * 4),
  mEnableMic(0),
  mRecorder(0),
  mSystem(0)//,
  //mStartTalkSound(0),
  //mStopTalkSound(0)
{
  FMOD_CHECK(FMOD::System_Create(&mSystem));

  FMOD_CHECK(mSystem->init(100, FMOD_INIT_NORMAL, 0));
  // 
  //   // Load our talk start/end sounds
  //   FMOD_CHECK(mSystem->createSound("media/starttalk.wav", FMOD_DEFAULT, 0, &mStartTalkSound));
  //   FMOD_CHECK(mSystem->createSound("media/stoptalk.wav", FMOD_DEFAULT, 0, &mStopTalkSound));

  // Create a recorder
  mRecorder = new VLFModSoundRecorder(*this);
}

VLFModSoundManager::~VLFModSoundManager()
{
  if (mSystem != 0)
  {
    if (mRecorder != 0)
    {
      delete mRecorder;
    }

    // If we assert here, stream players still exist, but we're shutting down FMOD - 
    // will likely cause pointer errors if we were to continue
    VL_ASSERT(mStreamPlayers.empty());

    // Release fmod
    mSystem->release();
  }
}

VLStaticSoundPtr VLFModSoundManager::createStaticSound(const VLString &source)
{
  VLStaticSoundPtr returnVal(new VLFModStaticSound(*this, source));

  return returnVal;
}

void VLFModSoundManager::process()
{
  updateIncomming();
  updateRecording();

  mSystem->update();
}

// void VLFModSoundManager::setCaptureEnabled(bool enabled)
// {
//   if (enabled)
//   {
//     VL_TRACE("BEGIN TRANSMIT");
//     //FMOD_CHECK(mSystem->playSound(FMOD_CHANNEL_FREE, mStartTalkSound, false, 0));
//   }
//   else
//   {
//     VL_TRACE("END_TRANSMIT");
//     //FMOD_CHECK(mSystem->playSound(FMOD_CHANNEL_FREE, mStopTalkSound, false, 0));
//   }
// 
//   mEnableMic = enabled;
// }

void VLFModSoundManager::updateIncomming()
{
  for (VLFModSoundStreamPlayerList::iterator iter = mStreamPlayers.begin();
    iter != mStreamPlayers.end(); ++iter)
  {
    (*iter)->process();
  }
}

void VLFModSoundManager::updateRecording()
{
  if (mRecorder != 0)
  {
    VLByteArrayPtrList frames;

    // Get the frames we wish to send
    mRecorder->pullData(frames);

    // Now send them to the network (remove the pointer to a list of bytes
    // and wrap it in an auto_ptr. It then gets sent to the network system
    for (VLByteArrayPtrList::iterator iter = frames.begin();
      iter != frames.end(); ++iter)
    {
      sendData(VLByteArrayPtr(*iter));
    }

    // Since we gave our pointers to someone else, we need to empty this list
    // before it is destroyed, or those pointers will be double-deleted
    frames.clear();
  }
}

void VLFModSoundManager::sendData(VLByteArrayPtr data)
{
  // Temporary hack to test - call receive data! The real implementation ought to
  // send network packets
  //receiveData(data);
  mOutgoingBuffer.give(data);
}

VLByteArrayPtr VLFModSoundManager::takeCapturedSoundData()
{
  return mOutgoingBuffer.takeAll();
}

float VLFModSoundManager::getTxVolume()
{
  float returnVal = 0.0f;

  //return mTxVolume;
  if (mRecorder != 0)
  {
    returnVal = mRecorder->getActivityLevel();
  }

  return returnVal;
}

VLSoundStreamPlayerPtr VLFModSoundManager::createSoundStreamPlayer( VLDecoderPtr decoder )
{
  std::auto_ptr<VLFModSoundStreamPlayer> returnValue(new VLFModSoundStreamPlayer(*this));

  mStreamPlayers.push_back(returnValue.get());

  return VLSoundStreamPlayerPtr(returnValue.release());
}

void VLFModSoundManager::unregisterSoundStreamPlayer( VLFModSoundStreamPlayer *player )
{
  // Remove this pointer from our list of sound stream players
  VLFModSoundStreamPlayerList::iterator iter = std::find(mStreamPlayers.begin(), mStreamPlayers.end(), player);

  if (iter != mStreamPlayers.end())
  {
    mStreamPlayers.erase(iter);
  }
  else
  {
    VL_ASSERT_ALWAYS();
  }
}